<template>
  <div class="x-upload">
    <div v-if="isSlots" class="slots">
      <n-upload v-bind="$props" @before-upload="onBeforeUpload" @finish="onFinish" @error="onError" @change="onChange">
        <slot />
      </n-upload>
    </div>
    <div v-else>
      <div class="upload">
        <div class="upload-card">
          <!--图片列表-->
          <vue-draggable
            v-model="imgList"
            tag="transition-group"
            :item-key="(element) => element"
            ghost-class="ghost"
            @Change="handleSortChange"
          >
            <template #item="{ element, index }">
              <UploadItem
                :is-video="data.mediaType === 2"
                :element="element"
                :disabled="disabled"
                :index="index"
                :style="getCSSProperties"
                :show-download="showDownload"
                @delete="handleRemove"
              />
            </template>
          </vue-draggable>
          <!--上传图片-->
          <div v-show="imgList.length < limit" class="uploader" :style="getCSSProperties">
            <n-upload
              v-bind="$props"
              @before-upload="onBeforeUpload"
              @finish="onFinish"
              @error="onError"
              @change="onChange"
            >
              <div class="flex center column" :style="getCSSProperties">
                <n-icon size="18">
                  <PlusOutlined />
                </n-icon>
                <div v-if="width > 60" class="upload-title">上传</div>
              </div>
            </n-upload>
          </div>
        </div>
      </div>
    </div>

    <n-space>
      <div v-if="restrictionText" class="restriction-text">{{ restrictionText }}</div>
    </n-space>

    <!--提示-->
    <n-space>
      <n-alert v-if="helpText" type="warning" class="flex">
        {{ helpText }}
      </n-alert>
    </n-space>
  </div>
</template>

<script lang="ts">
  import { defineComponent, toRefs, reactive, computed, watch } from 'vue'
  import { PlusOutlined } from '@vicons/antd'
  import { NUpload, NIcon, NSpace, NAlert, NSpin, useMessage } from 'naive-ui'
  import type { UploadFileInfo } from 'naive-ui'
  import { basicProps } from './props'
  import { isObject, isString } from '@/utils/is'
  import { formatCdn, getImageInfo, formatNumber } from '@/utils'
  import VueDraggable from 'vuedraggable'
  import UploadItem from './UploadItem.vue'

  export default defineComponent({
    name: 'BasicUpload',
    components: {
      NUpload,
      NIcon,
      NSpace,
      NAlert,
      PlusOutlined,
      NSpin,
      VueDraggable,
      UploadItem
    },
    props: {
      ...NUpload.props, // 这里继承原 UI 组件的 props
      ...basicProps,
      showDownload: {
        type: Boolean,
        default: false
      }
    },
    emits: ['success', 'update:value', 'remove', 'complete', 'start'],
    setup(props, { emit, slots }) {
      const imageTypes = ['image/jpg', 'image/png', 'image/jpeg', 'image/svg', 'image/gif', 'image/webp']
      const videoTypes = ['video/mp4', 'video/mov', 'video/avi']
      const getCSSProperties = computed(() => {
        return {
          width: `${props.width}px`,
          height: `${props.height}px`
        }
      })

      watch(
        () => props.value,
        (val, oldVal) => {
          if (val && !oldVal) {
            updateValue()
          }
        }
      )

      const message = useMessage()

      interface IState {
        count: number
        isLoading: boolean
        imgList: string[]
        containerPadding: string
        isSlots: any
        videoInfo: Record<string, any>
        uploadCount: number
        finishedCount: number
      }
      const state = reactive<IState>({
        count: 0,
        isLoading: false,
        imgList: [],
        containerPadding: props.width > 60 ? '8px' : '2px',
        // 组件是否有插槽内容；如果有，上传组件中的样式使用插槽的
        isSlots: slots?.default,
        videoInfo: {},
        uploadCount: 0,
        finishedCount: 0
      })

      updateValue()
      function updateValue() {
        if (!props.value) return
        //赋值默认图片显示
        if (isString(props.value)) {
          if (props.value) {
            state.imgList = props.value.split(/,(?=http)/).map((item) => getImgUrl(item))
          }
        } else {
          if (props.value.length) {
            state.imgList = props.value.map((item: string) => getImgUrl(item))
          }
        }
      }

      function getFormattedValue() {
        let updateValue
        const imgList = state.imgList
          .map((item) => {
            if (item?.id) {
              return item.fileUrl
            } else {
              return item
            }
          })
          .filter((item) => item)

        if (isString(props.value) || !props.value) {
          updateValue = imgList.join(',')
        } else {
          updateValue = imgList
        }
        return updateValue
      }

      //删除
      function handleRemove(index: number, url: string) {
        state.imgList.splice(index, 1)
        const updateValue = getFormattedValue()
        emit('update:value', updateValue)
        emit('remove', index, url)
      }

      //组装完整图片地址
      function getImgUrl(url: string): string {
        return url
      }

      //上传之前
      async function onBeforeUpload({ file }) {
        const fileInfo = file.file
        const { validate, limit, accept } = props
        // 加强校验文件后缀，不匹配的不让上传
        try {
          if (fileInfo.type) {
            const type = fileInfo.type.split('/')
            const fileName = fileInfo.name.split('.')
            if (!(accept.includes(type[1]) || accept.includes(fileName[fileName.length - 1]))) {
              message.warning(`请选择指定格式的文件，${accept}`)
              return false
            }
          }
        } catch (e) {
          console.warn('onBeforeUpload-> ', e)
        }

        const needValidate = isObject(validate) || Object.keys(validate).length > 0
        if (videoTypes.includes(fileInfo.type)) {
          const videoInfo = await getVideoInfo(fileInfo)
          state.videoInfo = {
            ...videoInfo,
            type: fileInfo.type,
            size: fileInfo.size / 1024
          }
        }

        if (needValidate) {
          const { fileSize } = validate
          if (fileSize) {
            const isSize = fileInfo.size / 1024 < fileSize
            if (!isSize) {
              message.error(`文件大小不能超过${bytesToSize(fileSize)}!`)
              return false
            }
          }

          if (imageTypes.includes(fileInfo.type)) {
            //  图片类型
            await validateImage(fileInfo, validate)
          } else if (videoTypes.includes(fileInfo.type)) {
            //  视频类型
            await validateVideo(state.videoInfo, validate)
          }
        }

        const currentNum = state.imgList.length
        if (currentNum >= limit) {
          message.warning(`最多选择${limit}张图片, 超出数量的图片已取消上传`)
          return false
        }
        state.uploadCount++
        state.imgList.push(file)
        state.isLoading = true
        emit('start')
        return true
      }

      function onChange({ file }) {
        if (file.status === 'uploading') {
          state.isLoading = true
        }
      }

      //上传结束
      function onFinish({ file, event }: { file: UploadFileInfo; event?: ProgressEvent }) {
        state.isLoading = false
        // 上传完成
        const { success, data, msg } = JSON.parse((event?.target as XMLHttpRequest).response)
        const messageText = msg || '上传失败'
        //成功
        const fileIndex = state.imgList.findIndex((item) => item?.id === file.id)
        if (success) {
          // 图片和视频类型
          if (/image\/|video\//.test(file.type)) {
            if (!props.isManual) {
              let imgUrl = getImgUrl(data.fileUrl || data)
              // state.imgList.push(imgUrl)
              state.imgList.forEach((item) => {
                if (item?.id === file.id) {
                  item.percentage = 100
                  item.fileUrl = imgUrl
                }
              })
              const updateValue = getFormattedValue()
              emit('update:value', updateValue)
            }
            let newData = {}
            if (!data.fileUrl) {
              newData.fileUrl = data
            }
            let retInfo = {
              fileIndex,
              ...newData
            }
            // 视频
            if (videoTypes.includes(file.type)) {
              retInfo = {
                ...retInfo,
                fileWidth: state.videoInfo.width,
                fileHeight: state.videoInfo.height,
                fileDuration: state.videoInfo.duration,
                fileSize: state.videoInfo.size,
                fileType: state.videoInfo.type
              }
              uploadFinished()
              emit('success', retInfo)
            } else {
              // 图片 imageUrl：ocr 的字段
              getImageInfo(retInfo.fileUrl || retInfo.imageUrl)
                .then((res: any) => {
                  const { width, height } = res
                  uploadFinished()
                  emit('success', {
                    ...retInfo,
                    fileWidth: width,
                    fileHeight: height
                  })
                })
                .catch(() => {
                  uploadFinished()
                  emit('success', {
                    ...retInfo
                  })
                })
            }
          } else {
            uploadFinished()
            // 其他文件类型
            emit('success', data)
          }
        } else {
          message.error(messageText)
          state.imgList.splice(fileIndex, 1)
          uploadFinished()
        }
      }

      function onError() {
        uploadFinished()
      }
      /**
       * 校验图片
       */
      function validateImage(file, restriction) {
        const { width, height, type } = restriction
        return new Promise<void>((resolve, reject) => {
          if (type) {
            const typeList = type.split(',')
            if (!typeList.includes(file.type)) {
              reject()
              return message.error(`图片类型不正确，请上传类型为${type.replaceAll('image/', '')}的图片`)
            }
          }
          if (width && height) {
            const _URL = window.URL || window.webkitURL
            const img = new Image()
            img.src = _URL.createObjectURL(file)
            img.onload = () => {
              const valid = img.width === width && img.height === height
              if (valid) {
                resolve()
              } else {
                reject()
                message.error(`请上传尺寸为 ${width}×${height}像素 的图片!`)
              }
            }
          } else {
            resolve()
          }
        })
      }

      /**
       * 校验视频
       * @param file
       * @param restriction
       * @returns {Promise<unknown>}
       */
      function validateVideo(videoInfo, restriction) {
        const { minWidth, minHeight, minDuration, maxDuration, ratioWidth, ratioHeight } = restriction
        return new Promise<void>((resolve, reject) => {
          const pixel = videoInfo.width >= minWidth && videoInfo.height >= minHeight
          const duration = minDuration < videoInfo.duration && videoInfo.duration < maxDuration
          const ratio = videoInfo.width / videoInfo.height === ratioWidth / ratioHeight
          if (!pixel && minWidth && minHeight) {
            reject()
            message.error(`视频尺寸须大于${minWidth}px*${minHeight}px!`)
          } else if (!duration && minDuration && maxDuration) {
            reject()
            message.error(`视频时长控制在${minDuration}-${maxDuration}内!`)
          } else if (!ratio && ratioWidth && ratioHeight) {
            reject()
            message.error(`请上传比例为${ratioWidth}:${ratioHeight}的视频!`)
          } else {
            resolve()
          }
        })
      }

      // 获取视频信息
      function getVideoInfo(file) {
        return new Promise<{ width: number; height: number; duration: number }>((resolve, reject) => {
          const _URL = window.URL || window.webkitURL
          const fileUrl = _URL.createObjectURL(file)
          const video = document.createElement('video')

          video.addEventListener('loadedmetadata', (e) => {
            _URL.revokeObjectURL(fileUrl)
            resolve({
              width: video.videoWidth,
              height: video.videoHeight,
              duration: video.duration
            })
          })
          video.src = fileUrl
        })
      }

      function bytesToSize(bytes: number): string {
        if (!bytes) return '0 KB'
        const k = 1024
        const sizes = ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
        const i = Math.floor(Math.log(bytes) / Math.log(k))
        return formatNumber(bytes / Math.pow(k, i)) + sizes[i]
      }

      function uploadFinished() {
        state.finishedCount++
        if (state.finishedCount >= state.uploadCount) {
          state.uploadCount = 0
          state.finishedCount = 0
          emit('complete')
        }
      }

      function handleSortChange() {
        const updateValue = getFormattedValue()
        emit('update:value', updateValue)
      }

      return {
        ...toRefs(state),
        onFinish,
        onError,
        onChange,
        onBeforeUpload,
        handleRemove,
        getCSSProperties,
        handleSortChange,
        formatCdn
      }
    }
  })
</script>

<style scoped lang="scss">
  .upload {
    width: 100%;
    overflow: hidden;

    &-card {
      width: auto;
      height: auto;
      display: flex;
      flex-wrap: wrap;
      align-items: center;

      .uploader {
        opacity: 1;
        margin: 0 8px 8px 0;
        border: 1px dashed #d9d9d9;
        border-radius: 4px;
        cursor: pointer;
        background: #fafafa;
        color: #666;
        padding: 0;
        transition: border-color 0.3s;

        .upload-box {
          display: flex;
          flex-direction: column;
          justify-content: center;
          align-items: center;
        }

        .upload-title {
          color: #666;
        }

        &:hover {
          transition: border-color 0.3s;
          border-color: #1890ff;
        }
      }
    }
  }
  .restriction-text {
    color: #9a9a9a;
  }
</style>
